Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow linker to perform deadcode elimination for program using Cobra #1956

Merged
merged 8 commits into from
Jan 27, 2025

Conversation

aarzilli
Copy link
Contributor

@aarzilli aarzilli commented May 5, 2023

Fixes #2015

Cobra, in its default configuration, will execute a template to generate help, usage and version outputs. Text/template execution calls MethodByName and MethodByName disables dead code elimination in the Go linker, therefore all programs that make use of cobra will be linked with dead code elimination disabled, even if they end up replacing the default usage, help and version formatters with a custom function and no actual text/template evaluations are ever made at runtime.

Dead code elimination in the linker helps reduce disk space and memory utilization of programs. For example, for the simple example program used by TestDeadcodeElimination 40% of the final executable size is dead code. For a more realistic example, 12% of the size of Delve's executable is deadcode.

This PR changes Cobra so that, in its default configuration, it does not automatically inhibit deadcode elimination by:

  1. changing Cobra's default behavior to emit output for usage and help using simple Go functions instead of template execution
  2. quarantining all calls to template execution into SetUsageTemplate, SetHelpTemplate and SetVersionTemplate so that the linker can statically determine if they are reachable

See https://github.com/aarzilli/whydeadcode to help find why dead-code-elimination may not work as well as it could in your project.

@CLAassistant
Copy link

CLAassistant commented May 5, 2023

CLA assistant check
All committers have signed the CLA.

@github-actions
Copy link

github-actions bot commented May 5, 2023

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@aarzilli
Copy link
Contributor Author

aarzilli commented May 5, 2023

The CLA thing isn't working for some reason.

@github-actions
Copy link

github-actions bot commented May 5, 2023

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@marckhouzam
Copy link
Collaborator

Thanks @aarzilli !
I like the idea of optimizing things.
This will require some thought so I can understand clearly the behaviour change. Thanks for adding a test it should help with this.

@aarzilli
Copy link
Contributor Author

Unless I made a mistake there shouldn't be any behavior changes (as in, observable from the outside). Happy to explain anything about this if needed.

@aarzilli
Copy link
Contributor Author

Ping?

1 similar comment
@aarzilli
Copy link
Contributor Author

Ping?

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@marckhouzam
Copy link
Collaborator

marckhouzam commented Oct 14, 2024

@aarzilli I apologize for such a long delay, but I had misunderstood the value of this PR. I now realize that it may help all programs using Cobra, so I'm very interested.

I'm trying to convince myself that that this change actually has an impact.
Would you be able to explain how I can see the difference in a program that has dead code, before and after this PR?

Here is what I tried.

  • I used the program you have added to the tests in this PR
  • I added an "Unused()" function that I don't call in the program.
  • I ran go build -o myprog . (using cobra 1.8.1 without your PR)
  • I ran go tool nm ./myprog | grep Unused

I would have expected to see the Unused function but I don't.
I used go version go1.22.8 darwin/arm64

Could you clarify what I should expect?

This is the program:

package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

func Unused() {
	fmt.Println("not called")
}

var rootCmd = &cobra.Command{
	Version: "1.0",
	Use:     "example_program",
	Short:   "example_program - test fixture to check that deadcode elimination is allowed",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("hello world")
	},
	Aliases: []string{"alias1", "alias2"},
	Example: "stringer --help",
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
		os.Exit(1)
	}
}

@marckhouzam marckhouzam changed the title Restructure code to let linker perform deadcode elimination step Allow linker to perform deadcode elimination for program using Cobra Oct 14, 2024
@aarzilli
Copy link
Contributor Author

aarzilli commented Oct 14, 2024

One easy way to see the impact of this change is to compile your example program with and without this PR:

$ ls -lh
total 8.4M
-rwxr-xr-x. 1 a a 5.2M Oct 14 14:45 example.withoutpr*
-rwxr-xr-x. 1 a a 3.2M Oct 14 14:45 example.withpr*
-rw-r--r--. 1 a a  596 Oct 14 14:44 main.go

This is an extreme example, where the size of the executable is reduced by 38%, but reductions of 10% are realistic.
I used go1.23 but you should get very similar results with go1.22.8.

As to your question, saying that deadcode elimination gets disabled is incorrect: in reality it continues to function but in a reduced capacity. Specifically if reflect.Value.MethodByName or reflect.Value.Method are reachable it can not always prove that exported methods are unreachable.

Your Unused function is not an exported method so, unless it is called by an exported method, it can always be deadcode eliminated.

If you want to see the Unused function make it all the way to the executable you have to do something like this:

package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

type Astruct struct {
	n int
}

//go:noinline
func Unused() {
	fmt.Println("not called")
}

//go:noinline
func (a *Astruct) Unused() {
	fmt.Println("not called", a.n)
	Unused()
}

//go:noinline
func (a *Astruct) Used() {
	fmt.Println("used", a.n)
}

var rootCmd = &cobra.Command{
	Version: "1.0",
	Use:     "example_program",
	Short:   "example_program - test fixture to check that deadcode elimination is allowed",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("hello world")
	},
	Aliases: []string{"alias1", "alias2"},
	Example: "stringer --help",
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err)
		os.Exit(1)
	}
	var a Astruct
	fmt.Println(a)
	a.Used()
}

All of the noinline directives are needed because small functions like this would be removed by the inliner and the fmt.Println call is needed to make the Astruct type itself reachable (if all calls to the methods of Astruct are static the linker can still prove that Unused is unreachable).

With these changes you get:

$ go build main2.go && go tool nm ./main2 | grep 'Unused'
  589c00 T main.(*Astruct).Unused
  589ba0 T main.Unused
  765570 D runtime.adviseUnused
  4179e0 T runtime.sysUnusedOS

without the PR and:

$ go build main2.go && go tool nm ./main2 | grep 'Unused'
  602564 D runtime.adviseUnused
  4144a0 T runtime.sysUnusedOS

with the PR.

@marckhouzam
Copy link
Collaborator

Thanks for the explanation @aarzilli, I can now see the benefits.

So, with this PR, programs that don't set new templates (usage, help or version) will be able to do dead code elimination fully.

What about programs that do modify those templates? If they want to get full usage of dead code elimination they should convert their template use to a go function like you have done, IIUC? My next step for this review is to try to override the help/usage/version in that fashion and see if it works as expected.

I believe that using SetHelpFunc() and SetUsageFunc() will allow programs to do this, but I don't think we have a way to do this for the version template. But it is ok, it is still better than not being able to do it at all. Once this is merged, we can discuss adding a SetVersionFunc().

Copy link
Collaborator

@marckhouzam marckhouzam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great.
I still want to do some testing, but I don't expect any big surprises.
Here are some comments for the PR.
The PR also needs to be rebased.

Thanks again for your patience, I think we can get this in soon.

@marckhouzam
Copy link
Collaborator

What do you think about updating the documentation in the three sections starting here https://github.com/spf13/cobra/blob/main/site/content/user_guide.md#defining-your-own-help to explain the side-effect of overriding the template, and therefore that it is recommended to set a function instead?

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@aarzilli
Copy link
Contributor Author

Thanks for the explanation @aarzilli, I can now see the benefits.

So, with this PR, programs that don't set new templates (usage, help or version) will be able to do dead code elimination fully.

What about programs that do modify those templates? If they want to get full usage of dead code elimination they should convert their template use to a go function like you have done, IIUC?

Yes, this is correct.

I believe that using SetHelpFunc() and SetUsageFunc() will allow programs to do this, but I don't think we have a way to do this for the version template. But it is ok, it is still better than not being able to do it at all. Once this is merged, we can discuss adding a SetVersionFunc().

It imagine should be trivial.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@aarzilli
Copy link
Contributor Author

The changes look good to me.

@marckhouzam marckhouzam merged commit 611e16c into spf13:main Jan 27, 2025
20 checks passed
@artem-anisimov-0x7f
Copy link

This is a very good improvement. @marckhouzam, could you please make a new release of cobra?

@marckhouzam
Copy link
Collaborator

@aarzilli thank you for your patience. This took a very long time but you remained very responsive. Your nice attitude (and great technical expertise) made it possible to get this merged. This should benefit many programs!

P.S. I wouldn't be against adding a section to the Cobra documentation teaching people what they should do to try to get dead-code-elimination working. Although this is more of a general Go concept, it might help people understand it if they have a small section in the Cobra docs. But I leave that up to you, if you still have energy after this looooong review.

@marckhouzam
Copy link
Collaborator

This is a very good improvement. @marckhouzam, could you please make a new release of cobra?

Nice to see immediate interest 😄 .
I'll work on a release this week.

@aarzilli
Copy link
Contributor Author

Thanks for getting this merged!

@sylr
Copy link

sylr commented Jan 29, 2025

@jpmcb Could we get a cobra release soon to be able to enjoy this change ?

marckhouzam added a commit to marckhouzam/cobra that referenced this pull request Feb 2, 2025
Follow-up to spf13#1956.

This commit allows a program to reset any of the tree templates to their
default behaviour, as it was possible to do before the change of spf13#1956.

Signed-off-by: Marc Khouzam <[email protected]>
jpmcb pushed a commit that referenced this pull request Feb 6, 2025
Follow-up to #1956.

This commit allows a program to reset any of the tree templates to their
default behaviour, as it was possible to do before the change of #1956.

Signed-off-by: Marc Khouzam <[email protected]>
@artem-anisimov-0x7f
Copy link

Is there a chance to have v1.9.0 released soon to make this fix available?

sylr added a commit to sylr/jsm.go that referenced this pull request Feb 13, 2025
This modules uses `github.com/expr-lang/expr` which, because of the
extensive use of the `reflect` package, disables go's compiler dead code
elimination which can lead to bigger binaries.

This commit adds a `noexprlang` build tag that allows jsm.go users that
do not use expression matching to entirely disable the use of the expr
module so that they can benefit from go's dead code elimination if they
are eligible to outside jsm.go.

References:
- https://golab.io/talks/getting-the-most-out-of-dead-code-elimination
- https://github.com/aarzilli/whydeadcode
- spf13/cobra#1956

Signed-off-by: Sylvain Rabot <[email protected]>
sylr added a commit to sylr/jsm.go that referenced this pull request Feb 13, 2025
This modules uses `github.com/expr-lang/expr` which, because of the
extensive use of the `reflect` package, disables go's compiler dead code
elimination which can lead to bigger binaries.

This commit adds a `noexprlang` build tag that allows jsm.go users that
do not use expression matching to entirely disable the use of the expr
module so that they can benefit from go's dead code elimination if they
are eligible to outside jsm.go.

References:
- https://golab.io/talks/getting-the-most-out-of-dead-code-elimination
- https://github.com/aarzilli/whydeadcode
- spf13/cobra#1956

Signed-off-by: Sylvain Rabot <[email protected]>
sylr added a commit to sylr/jsm.go that referenced this pull request Feb 13, 2025
This module uses `github.com/expr-lang/expr` which, because of the
extensive use of the `reflect` package, disables go's compiler dead code
elimination which can lead to bigger binaries.

This commit adds a `noexprlang` build tag that allows jsm.go users that
do not use expression matching to entirely disable the use of the expr
module so that they can benefit from go's dead code elimination if they
are eligible to outside jsm.go.

References:
- https://golab.io/talks/getting-the-most-out-of-dead-code-elimination
- https://github.com/aarzilli/whydeadcode
- spf13/cobra#1956

Signed-off-by: Sylvain Rabot <[email protected]>
@marckhouzam
Copy link
Collaborator

🎉 https://github.com/spf13/cobra/releases/tag/v1.9.0
Thanks @aarzilli. The dead code elimination improvement will be valuable to many

aarzilli added a commit to aarzilli/delve that referenced this pull request Feb 17, 2025
When reflect.MethodByName is used the linker can not fully perform
deadcode elimination. This commit updates cobra and rewrites the
suitableMethods part of service/rpccommon so that reflect.MethodByName
is not used and the linker can fully execute deadcode elimination.

The executable size on go1.24.0 on linux is reduced from 25468606 bytes
to 22453382 bytes or a reduction of approximately 12%.

See also:

spf13/cobra#1956
https://github.com/aarzilli/whydeadcode
derekparker pushed a commit to go-delve/delve that referenced this pull request Feb 18, 2025
When reflect.MethodByName is used the linker can not fully perform
deadcode elimination. This commit updates cobra and rewrites the
suitableMethods part of service/rpccommon so that reflect.MethodByName
is not used and the linker can fully execute deadcode elimination.

The executable size on go1.24.0 on linux is reduced from 25468606 bytes
to 22453382 bytes or a reduction of approximately 12%.

See also:

spf13/cobra#1956
https://github.com/aarzilli/whydeadcode
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 18, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because tamplating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 18, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because templating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 18, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because templating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 19, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because templating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 19, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because templating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to marckhouzam/tanzu-plugin-runtime that referenced this pull request Feb 19, 2025
When templating is used, the linker cannot know which functions are
called and which one are not; this is because templating use calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
marckhouzam added a commit to vmware-tanzu/tanzu-plugin-runtime that referenced this pull request Feb 19, 2025
When templating is used, the linker cannot know which functions are
called and which ones are not; this is because templating uses calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
ajitgunturi pushed a commit to ajitgunturi/tanzu-plugin-runtime that referenced this pull request Feb 25, 2025
…#228)

When templating is used, the linker cannot know which functions are
called and which ones are not; this is because templating uses calls to
"MethodByName()" which can be used to call any function (similar to
reflection).

With the release of Cobra 1.9.1, templates are no longer used by default
in Cobra. By also not using templates in the tanzu-plugin-runtime it now
gives an opportunity for plugins to try to avoid templates to allow
dead-code elimination to work, if they so choose.

Ref: spf13/cobra#1956

Signed-off-by: Marc Khouzam <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The use of text/template disables dead code elimination in all users of cobra
7 participants